Skip to content

S03-02 JS-基础-函数

[TOC]

函数

概述

元语法变量

元语法变量(Metasyntactic Variables):是编程领域中的一种特殊命名惯例,用于在示例代码、文档和教学材料中作为通用占位符。这些变量名本身没有特定含义,但已成为开发者社区广泛接受的标准化术语。

核心成员

  • foo - 最基础、最常用的占位符
  • bar - foo 的黄金搭档
  • baz - 第三个常用占位符
  • qux - 第四个占位符
  • quux - 第五个占位符

由来:foo、bar这些名词最早从什么时候、地方流行起来的一直是有争论的:

  • 说法一:是通过Digital(迪吉多,数字设备公司,成立于1957年的美国电脑公司)的手册说明流行起来的;

  • 说法二:是说源自于电子学中的反转foo信号;

  • 说法三:是foo因为出现在了一个漫画中,漫画中foo代表“好运”,与中文的福读音类似;

  • 总之:foo、bar、baz已经是编程领域非常常用的名词。我个人也比较习惯在写一些变量、函数名词时使用这些词汇,大家做一个了解;

认识函数

函数(Function):是一段可重复使用的代码块,用于执行特定任务或计算值。函数允许您封装逻辑、提高代码复用率、简化复杂操作并组织程序结构。

核心价值

  1. 代码复用:避免重复编写相同代码
  2. 模块化:将复杂问题分解为小任务
  3. 抽象:隐藏实现细节,暴露简洁接口
  4. 作用域管理:创建独立变量环境

函数分类

  • 内置函数:JS引擎或者浏览器内部提供的已经实现好的函数。
  • 自定义函数:自己编写的函数。

目前, 我们已经接触过几个函数了

  • alert():浏览器弹出一个弹窗。
  • prompt():在浏览器弹窗中接收用户的输入。
  • console.log():在控制台输入内容。
  • String() / Number() / Boolean()

函数使用步骤

函数使用步骤:函数的使用包含两个步骤:

  • 声明函数:封装独立的功能
  • 调用函数:享受封装的成果

声明函数(定义函数):声明函数的过程是对某些功能的封装过程。

  • 在之后的开发中,我们会根据自己的需求定义很多自己的函数。

调用函数(函数调用):调用函数是让已存在的函数为我们所用;

  • 这些函数可以是刚刚自己封装好的某个功能函数。
  • 当然, 我们也可以去使用默认提供的或者其他三方库定义好的函数。

函数的作用:在开发程序时,使用函数可以提高编写的效率以及代码的重用。

定义函数

函数可以通过以下方式定义:

  • 函数表达式(Function Expressions)
  • 函数表达式(Function Expressions)
函数声明

函数声明(Function Declaration):使用 function 关键字后跟函数名来创建命名函数。函数声明在代码执行前就会被解析和提升(hoisted),使得你可以在声明前调用函数。

语法

js
function functionName(parameter1, parameter2, ...) {
    // 函数体 - 执行代码
    return result; // 可选返回值
}
  • function 关键字:标识函数声明的开始
  • 函数名:函数的标识符,遵循变量命名规则
  • 参数列表:括号内的零个或多个参数,逗号分隔
  • 函数体:花括号 {} 内的代码块
  • 返回值:可选的 return 语句,用于返回结果
函数表达式

在JavaScript中,函数并不是一种神奇的语法结构,而是一种特殊的值

前面定义函数的方式,我们称之为函数的声明(Function Declaration);

还有另外一种写法是函数表达式(Function Expressions)。

函数表达式(Function Expressions):是 JS 中将函数定义为表达式的一部分的语法形式。与函数声明不同,函数表达式不会被提升(hoisted),而是在代码执行到该表达式时才创建函数。

语法

js
// 函数表达式
const functionName = function(parameters) {
    // 函数体
    return result;
};

核心特性

  • 匿名性:可创建匿名函数(也可命名)。
  • 函数作为值存在:函数是作为一个赋值给变量的值存在的,该值是一个Function对象类型。
  • 不可提升:不同于函数声明,函数表达式不能在定义前调用。

函数声明 vs 函数表达式

在开发中,函数的声明和函数表达式有什么区别,以及如何选择呢?

  1. 语法不同

    • 函数声明:在主代码流中声明为单独的语句的函数。

    • 函数表达式:在一个表达式中或另一个语法结构中创建的函数。

  2. 创建函数的时机不同

    • 函数表达式

      代码执行到达时被创建,并且仅从那一刻起可用。

    • 函数声明

      被定义之前就可以被调用。

      这是内部算法的原故,当 JS 准备运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数;

  3. 开发中如何选择:首先考虑函数声明语法,它更灵活,可以在定义之前被调用

函数调用

函数调用(Function Invocation):指的是触发函数执行的动作,通过函数名后跟括号 () 实现。可传递参数(Arguments)给函数,可接收返回值(Return Value)。

函数定义完后里面的代码是不会执行的,函数必须调用才会执行;

语法

js
functionName(arg1, arg2, ...)

执行流程

  1. 创建执行上下文:为函数调用创建新的作用域
  2. 参数传递:将实参值赋给形参变量
  3. 控制权转移:程序计数器跳转到函数入口
  4. 执行函数体:按顺序执行函数内代码
  5. 返回值处理
    • 遇到 return 语句,返回指定值
    • return 则返回 undefined
  6. 上下文销毁:完成执行后清除局部变量
  7. 恢复执行:返回到调用点继续执行

参数传递机制

  • 按值传递:基本类型传递副本

    js
    // 值传递
    let num = 10;
    function changeValue(val) {
        val = 20; // 不影响外部变量
    }
    changeValue(num);
    console.log(num); // 10
  • 按引用传递:对象传递引用(非真实引用)

    js
    // "引用"传递
    let obj = { value: 10 };
    function changeProperty(objRef) {
        objRef.value = 20; // 修改共享对象
    }
    changeProperty(obj);
    console.log(obj.value); // 20
  • 参数解构:ES6 高级特性

执行上下文与作用域

  • 每次调用创建新执行上下文

  • 形成作用域链(Scope Chain)

  • 确定 this 的值

    js
    const car = {
        brand: "Toyota",
        start: function() {
            console.log(`Starting ${this.brand}`); // this = car
        }
    };
    
    car.start(); // "Starting Toyota"

函数组成

参数

函数的参数:是函数定义时声明的变量,用于接收传入函数的值。参数允许我们向函数传递数据,使函数能够根据不同的输入执行相同的操作,从而增加代码的复用性和灵活性。

分类

  • 形参(Parameters):函数定义时声明的变量。在函数内部会把形参当做变量使用。
  • 实参(Arguments):函数调用时实际传入的值。需要按照定义时的顺序依次传递。

语法

js
// 形参: a, b
function add(a, b) {
  return a + b;
}

// 实参: 3, 5
const result = add(3, 5);

返回值

函数的返回值:是指函数执行完成后返回给调用者的数据。在JS中,每个函数调用都会返回一个值,即使没有显式使用return语句。

语法

  1. 显式返回:使用 return 语句显式指定返回值

    js
    // 1. 使用 return 语句显式指定返回值
    function add(a, b) {
      return a + b; // 返回两数之和
    }
    
    const result = add(3, 5); // result = 8
  2. 隐式返回:没有return语句,或者return后没有值,则默认返回undefined

    • 仅用于副作用的函数(如修改 DOM、日志记录)通常不返回值
    js
    // 2. 没有return语句的函数
    function noReturn() {
      // 什么都不做
    }
    console.log(noReturn()); // undefined
    
    // 2. return后没有值
    function emptyReturn() {
      return;
    }
    console.log(emptyReturn()); // undefined

return:具有以下特性:

  • 立即终止函数:执行到 return 时函数立即结束
  • 只能返回单个值:但可通过数组/对象返回多个值
  • 位置灵活:可在函数任意位置使用

返回多个值:通过数组或对象返回多个结果。

js
// 使用数组
function calculate(a, b) {
  return [
    a + b, // 和
    a - b, // 差
    a * b, // 积
    a / b  // 商
  ];
}
const [sum, difference, product, quotient] = calculate(10, 2);
js
// 使用对象
function getDimensions() {
  return {
    width: window.innerWidth,
    height: window.innerHeight,
    ratio: window.innerWidth / window.innerHeight
  };
}
const {width, height, ratio} = getDimensions();

链式返回值:返回对象自身以支持链式调用。

js
class Calculator {
  constructor(value = 0) {
    this.value = value;
  }
  
  add(num) {
    this.value += num;
    return this; // 返回自身实例
  }
  
  multiply(num) {
    this.value *= num;
    return this;
  }
}

const result = new Calculator(5)
  .add(3)      // 5 + 3 = 8
  .multiply(2) // 8 * 2 = 16
  .add(10)     // 16 + 10 = 26
  .value;      // 最终结果: 26

异步返回值:处理异步操作返回值。

js
// 返回Promise
function fetchData(url) {
  return fetch(url)
    .then(response => response.json());
}

// 使用async/await
async function getUser(userId) {
  try {
    const response = await fetchData(`/users/${userId}`);
    return await response.json();
  } catch (error) {
    return { error: "Failed to fetch user" };
  }
}

递归函数

函数中调用函数

函数中调用另一个函数

  • 在开发中,函数内部是可以调用另外一个函数的。

  • 该函数可以是内置函数,也可以是自定义函数。

image-20250625230052020

函数中调用自己

  • 函数中也可以调用自己。

  • 函数调用自己必须有结束条件,否则会产生无限调用,造成报错。

image-20250625230820770

递归

递归(Recursion)函数调用自己也称为递归。

语法:每个递归函数包含两个关键部分:

js
function recursiveFunction(params) {
  // 1. 基本条件(Base Case) - 递归终止条件
  if (baseCaseCondition) {
    return baseCaseValue;
  }
  
  // 2. 递归步骤(Recursive Step) - 分解问题并自我调用
  return process(params) + recursiveFunction(modifiedParams);
}

递归三要素

  1. 明确功能

    首先要明确函数的功能,即这个函数要完成什么任务。例如,计算一个数的阶乘。

  2. 确定结束条件

    递归必须有一个结束条件,否则会陷入无限循环。例如,计算阶乘时,当 n 为 1 时,直接返回 1。

  3. 找到等价关系式

    通过缩小问题的规模,找到原函数的等价关系式。例如,阶乘的等价关系式为 f(n) = n * f(n-1)。

语言学描述递归:递归读取下面的话:

  • 从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?

递归是一种重要的编程思想:将一个复杂的任务,转化成可以重复执行的相同任务;

示例:自定义幂函数:封装一个函数,实现x的n次方。

  1. for循环实现

    image-20250626150454575

  2. 递归实现:如下:

递归实现

递归实现:另一种实现思路是递归实现:

1、递归实现的数学原理公式:xⁿ = x * xⁿ⁻¹

2、递归实现

image-20250626151542183

3、问题:上述的递归缺少了结束条件,会造成死循环

image-20250626151638708

4、添加结束条件,递归触发该条件时不再继续

image-20250626151730436

5、原理图

image-20250519104502179

递归的代码第一次接触会有点绕,对于初次接触函数的同学,可以先跳过去。

  • 后续我们讲解数据结构与算法时,会使用递归来解决一些算法问题;

示例:递归实现斐波那契数列

  1. 斐波那契数列

    • 示例1 1 2 3 5 8 13 21 34 55 89 ...
    • 数学规律F(n) = F(n-1) + F(n-2), F(1) = 1,F(2)=1 ,从第三个数开始,当前的数等于前2个数的和。
  2. 递归实现代码

    image-20250626153208100

  3. for循环实现代码

    image-20250626154112909

作用域

作用域(Scope):表示一些标识符的作用有效范围(所以也有被翻译为有效范围的);

在JavaScript(ES5之前)中没有块级作用域的概念,但是函数可以定义自己的作用域

函数的作用域:表示在函数内部定义的变量,只有在函数内部可以被访问到。

局部变量(Local Variables):定义在函数内部的变量。

外部变量(Outer Variables):定义在函数外部的变量。

全局变量:在函数之外声明的变量(在script中声明的)。

  • 全局变量在任何函数中都是可见的。
  • 通过var声明的全局变量会在window对象上添加一个属性(了解);

变量访问顺序:在函数中,访问变量的顺序是什么呢?

  • 优先访问自己函数中的变量,没有找到时,在外部中访问。

进阶知识:关于块级作用域、作用域链、变量提升、AO、VO、GO等概念我们后续将进行学习。

头等函数

头等函数(First-class Function):是指在程序设计语言中,函数被当作头等公民。函数可以作为别的函数的参数函数的返回值赋值给变量存储在数据结构中,有人主张也应包括支持匿名函数。

核心特性

  1. 赋值给变量:函数可以被赋值给变量,然后通过该变量调用。

    js
    const greet = function() {
        console.log("Hello!");
    };
    greet();  // 输出: Hello!
  2. 作为参数传递给其他函数

    js
    function sayHello(callback) {
        callback();
    }
    
    sayHello(function() {
        console.log("Hello from callback!");
    });  // 输出: Hello from callback!
  3. 作为返回值返回

    js
    function outer() {
        return function inner() {
            console.log("I'm the inner function!");
        };
    }
    
    const fn = outer();
    fn();  // 输出: I'm the inner function!
  4. 存储在数据结构中:函数可以作为元素存储在数组、对象等数据结构中。

    js
    const functions = [
        function() { console.log("First function!"); },
        function() { console.log("Second function!"); }
    ];
    
    functions[0]();  // 输出: First function!
    functions[1]();  // 输出: Second function!

函数式编程

函数式编程(Functional Programming,FP):是一种以函数为核心的编程范式,它强调纯函数不可变数据函数组合

核心原则

  1. 纯函数(Pure Function)

  2. 不可变性 (Immutability):数据一旦创建不可更改,修改数据时创建新副本

    js
    // 可变操作(不推荐)
    const arr = [1, 2, 3];
    arr.push(4); // 修改原数组
    
    // 不可变操作(推荐)
    const newArr = [...arr, 4]; // 创建新数组
  3. 函数组合 (Function Composition)

函数分类

回调函数

回调函数(Callback Function):是指一个被作为参数传递给另一个函数,并且预期在特定事件发生、条件满足或某个操作完成时被调用(“回调”) 的函数。

核心特性

  • 函数作为参数:JS 中函数是“一等公民”,可以像变量一样被赋值、传递。
  • 延迟执行/按需调用:接收回调函数的函数(通常称为“高阶函数”或“调用函数”)决定何时以及如何调用这个回调函数。
  • 通知机制:回调函数常用于通知调用者某个异步操作(如读取文件、网络请求、定时器)已完成,或者某个事件(如用户点击、数据到达)已发生。
  • 控制反转:调用函数控制了回调的执行时机和上下文,而不是由定义回调的代码直接控制。

应用场景

  1. 异步操作

    js
    setTimeout(function() {
        console.log('这段代码在 1 秒后执行!');
    }, 1000); // 匿名函数作为回调传递给 setTimeout
  2. DOM事件监听

    js
    document.getElementById('myButton').addEventListener('click', function(event) {
        alert('按钮被点击了!'); // 匿名函数作为点击事件的回调
    });
  3. 数组迭代方法:这些回调是同步执行的:forEach, map, filter, reduce, find, some, every

    js
    const numbers = [1, 2, 3, 4];
    const doubled = numbers.map(function(number) { // 匿名函数作为 map 的回调
        return number * 2;
    });
    console.log(doubled); // 输出: [2, 4, 6, 8]
  4. 自定义高阶函数

    js
    function calculate(num1, num2, operationCallback) { // operationCallback 是回调
        return operationCallback(num1, num2);
    }
    function add(a, b) {
        return a + b;
    }
    function multiply(a, b) {
        return a * b;
    }
    const sum = calculate(5, 3, add); // 传递 add 函数作为回调
    const product = calculate(5, 3, multiply); // 传递 multiply 函数作为回调
    console.log(sum); // 输出: 8
    console.log(product); // 输出: 15

高阶函数

高阶函数(Higher-Order Function):是函数式编程的核心概念,指操作其他函数的函数,它满足以下任一条件:

  • 接受一个或多个函数作为输入(参数)
  • 输出(返回)一个函数

语法

js
// 高阶函数的两种形式
const higherOrderFunc1 = (callback) => {  // 1. 接受函数作为参数
    return callback();
};

const higherOrderFunc2 = () => {          
    return () => console.log("返回的函数"); // 2. 返回函数作为结果
};

高阶函数类型

  1. 函数作为参数(回调模式)

    js
    // 1. 事件处理
    button.addEventListener('click', () => {
        console.log('按钮被点击');
    });
    
    // 2. 数组方法
    const numbers = [1, 2, 3];
    const doubled = numbers.map(num => num * 2); // [2, 4, 6]
    
    // 3. 异步操作
    fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(data => console.log(data));
  2. 函数作为返回值(函数工厂)

    js
    // 1. 创建乘法器
    const multiplier = factor => num => num * factor;
    const double = multiplier(2);
    const triple = multiplier(3);
    
    console.log(double(5)); // 10
    console.log(triple(5)); // 15
    
    // 2. 创建校验器
    const createValidator = rule => value => rule(value);
    const isEmail = createValidator(v => v.includes('@'));
    console.log(isEmail('test@example.com')); // true

匿名函数【

匿名(anonymous)函数的理解:

  • 如果在传入一个函数时,我们没有指定这个函数的名词或者通过函数表达式指定函数对应的变量,那么这个函数称之为匿名函数。

立即执行函数

语法特性

立即执行函数(Immediately Invoked Function Expression,IIFE,立即调用函数表达式):是 JavaScript 中一种定义后立即执行的函数模式。

语法

  • 不带参数

    js
    (function() {
        // 函数体 - 立即执行
    })();
  • 带参数

    js
    // 带参数形式
    (function(param) {
        console.log(param);
    })("参数值");
  • 常见变体

    js
    // 括号位置变体
    (function() {})();
    (function() {}());
    !function() {}();
    +function() {}();
    void function() {}();
  • 第一部分:定义了一个匿名函数,这个函数有自己独立的作用域。

  • 第二部分:后面的(),表示这个函数被执行了。

核心特性

  1. 创建独立的作用域:避免变量冲突,实现私有变量。

    • 内部可以访问/修改外部变量。
    • 外部不可以访问/修改内部变量。
    js
    // 全局命名空间
    var globalVar = "全局变量";
    
    (function() {
        var localVar = "局部变量";
        console.log(globalVar); // 可访问全局变量
        console.log(localVar);  // "局部变量"
    })();
    
    console.log(localVar); // ReferenceError: localVar is not defined
应用场景
  1. 解决循环中的闭包问题

    image-20250519104609279

注意事项
  1. 始终使用分号开头:防止与前一行代码合并

    js
    // 防止与前一行代码合并
    ;(function() {
        // ...
    })();
  2. 合理命名 IIFE:便于调试

    js
    // 便于调试
    (function initApp() {
        console.log("应用初始化");
        // 调用栈显示 initApp
    })();
  3. 作用域泄露:忘记 var/let/const

    js
    // 错误:忘记 var/let/const
    (function() {
        temp = "临时变量"; // 创建全局变量!
    })();
    
    console.log(temp); // "临时变量" (污染全局)
    
    // 正确:声明变量
    (function() {
        const temp = "局部变量";
    })();
  4. this 指向问题

    问题:默认 this 指向全局

    js
    const obj = {
        value: 10,
        init: function() {
            (function() {
                console.log(this.value); // undefined (this指向全局)
            })();
        }
    };

    解决方案

    • 保存 this

      js
      // 解决方案1:保存 this
      init: function() {
          const self = this;
          (function() {
              console.log(self.value); // 10
          })();
      }
    • 使用箭头函数

      js
      // 解决方案2:箭头函数
      init: function() {
          (() => {
              console.log(this.value); // 10 (箭头函数继承this)
          })();
      }
变体写法

立即执行函数必须是一个表达式(整体),不能是函数声明

  1. 错误写法:括号出现在匿名函数的末尾:

    • 此处的函数会被当成函数声明。
    • 此处的()表示优先级

    image-20250626221215180

  2. 变体写法:默认将函数作为表达式去解析,而不是函数声明。

    1. 括号包裹整个匿名/命名函数表达式和调用

      image-20250626221806002

    2. 括号包裹前面的匿名/命名函数表达式

      image-20250626222021229

    3. 使用 + / ! / void 将函数声明转换为函数表达式

      image-20250626222126524

代码风格

image-20250519104655357

debug调试

image-20250519104707612